/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.util;



import com.bff.gaia.unified.vendor.guava.com.google.common.base.MoreObjects;

import org.joda.time.Duration;



import static com.bff.gaia.unified.vendor.guava.com.google.common.base.Preconditions.checkArgument;



/**

 * A fluent builder for {@link BackOff} objects that allows customization of the retry algorithm.

 *

 * @see #DEFAULT for the default configuration parameters.

 */

public final class FluentBackoff {



  private static final double DEFAULT_EXPONENT = 1.5;

  private static final double DEFAULT_RANDOMIZATION_FACTOR = 0.5;

  private static final Duration DEFAULT_MIN_BACKOFF = Duration.standardSeconds(1);

  private static final Duration DEFAULT_MAX_BACKOFF = Duration.standardDays(1000);

  private static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE;

  private static final Duration DEFAULT_MAX_CUM_BACKOFF = Duration.standardDays(1000);



  private final double exponent;

  private final Duration initialBackoff;

  private final Duration maxBackoff;

  private final Duration maxCumulativeBackoff;

  private final int maxRetries;



  /**

   * By default the {@link BackOff} created by this builder will use exponential backoff (base

   * exponent 1.5) with an initial backoff of 1 second. These parameters can be overridden with

   * {@link #withExponent(double)} and {@link #withInitialBackoff(Duration)}, respectively, and the

   * maximum backoff after exponential increase can be capped using {@link

   * FluentBackoff#withMaxBackoff(Duration)}.

   *

   * <p>The default {@link BackOff} does not limit the number of retries. To limit the backoff, the

   * maximum total number of retries can be set using {@link #withMaxRetries(int)}. The total time

   * spent in backoff can be time-bounded as well by configuring {@link

   * #withMaxCumulativeBackoff(Duration)}. If either of these limits are reached, calls to {@link

   * BackOff#nextBackOffMillis()} will return {@link BackOff#STOP} to signal that no more retries

   * should continue.

   */

  public static final FluentBackoff DEFAULT =

      new FluentBackoff(

          DEFAULT_EXPONENT,

          DEFAULT_MIN_BACKOFF,

          DEFAULT_MAX_BACKOFF,

          DEFAULT_MAX_CUM_BACKOFF,

          DEFAULT_MAX_RETRIES);



  /**

   * Instantiates a {@link BackOff} that will obey the current configuration.

   *

   * @see FluentBackoff

   */

  public BackOff backoff() {

    return new BackoffImpl(this);

  }



  /**

   * Returns a copy of this {@link FluentBackoff} that instead uses the specified exponent to

   * control the exponential growth of delay.

   *

   * <p>Does not modify this object.

   *

   * @see FluentBackoff

   */

  public FluentBackoff withExponent(double exponent) {

    checkArgument(exponent > 0, "exponent %s must be greater than 0", exponent);

    return new FluentBackoff(

        exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries);

  }



  /**

   * Returns a copy of this {@link FluentBackoff} that instead uses the specified initial backoff

   * duration.

   *

   * <p>Does not modify this object.

   *

   * @see FluentBackoff

   */

  public FluentBackoff withInitialBackoff(Duration initialBackoff) {

    checkArgument(

        initialBackoff.isLongerThan(Duration.ZERO),

        "initialBackoff %s must be at least 1 millisecond",

        initialBackoff);

    return new FluentBackoff(

        exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries);

  }



  /**

   * Returns a copy of this {@link FluentBackoff} that limits the maximum backoff of an individual

   * attempt to the specified duration.

   *

   * <p>Does not modify this object.

   *

   * @see FluentBackoff

   */

  public FluentBackoff withMaxBackoff(Duration maxBackoff) {

    checkArgument(

        maxBackoff.getMillis() > 0, "maxBackoff %s must be at least 1 millisecond", maxBackoff);

    return new FluentBackoff(

        exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries);

  }



  /**

   * Returns a copy of this {@link FluentBackoff} that limits the total time spent in backoff

   * returned across all calls to {@link BackOff#nextBackOffMillis()}.

   *

   * <p>Does not modify this object.

   *

   * @see FluentBackoff

   */

  public FluentBackoff withMaxCumulativeBackoff(Duration maxCumulativeBackoff) {

    checkArgument(

        maxCumulativeBackoff.isLongerThan(Duration.ZERO),

        "maxCumulativeBackoff %s must be at least 1 millisecond",

        maxCumulativeBackoff);

    return new FluentBackoff(

        exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries);

  }



  /**

   * Returns a copy of this {@link FluentBackoff} that limits the total number of retries, aka the

   * total number of calls to {@link BackOff#nextBackOffMillis()} before returning {@link

   * BackOff#STOP}.

   *

   * <p>Does not modify this object.

   *

   * @see FluentBackoff

   */

  public FluentBackoff withMaxRetries(int maxRetries) {

    checkArgument(maxRetries >= 0, "maxRetries %s cannot be negative", maxRetries);

    return new FluentBackoff(

        exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries);

  }



  @Override

  public String toString() {

    return MoreObjects.toStringHelper(FluentBackoff.class)

        .add("exponent", exponent)

        .add("initialBackoff", initialBackoff)

        .add("maxBackoff", maxBackoff)

        .add("maxRetries", maxRetries)

        .add("maxCumulativeBackoff", maxCumulativeBackoff)

        .toString();

  }



  private static class BackoffImpl implements BackOff {



    // Customization of this backoff.

    private final FluentBackoff backoffConfig;

    // Current state

    private Duration currentCumulativeBackoff;

    private int currentRetry;



    @Override

    public void reset() {

      currentRetry = 0;

      currentCumulativeBackoff = Duration.ZERO;

    }



    @Override

    public long nextBackOffMillis() {

      // Maximum number of retries reached.

      if (currentRetry >= backoffConfig.maxRetries) {

        return STOP;

      }

      // Maximum cumulative backoff reached.

      if (currentCumulativeBackoff.compareTo(backoffConfig.maxCumulativeBackoff) >= 0) {

        return STOP;

      }



      double currentIntervalMillis =

          Math.min(

              backoffConfig.initialBackoff.getMillis()

                  * Math.pow(backoffConfig.exponent, currentRetry),

              backoffConfig.maxBackoff.getMillis());

      double randomOffset =

          (Math.random() * 2 - 1) * DEFAULT_RANDOMIZATION_FACTOR * currentIntervalMillis;

      long nextBackoffMillis = Math.round(currentIntervalMillis + randomOffset);

      // Cap to limit on cumulative backoff

      Duration remainingCumulative =

          backoffConfig.maxCumulativeBackoff.minus(currentCumulativeBackoff);

      nextBackoffMillis = Math.min(nextBackoffMillis, remainingCumulative.getMillis());



      // Update state and return backoff.

      currentCumulativeBackoff = currentCumulativeBackoff.plus(nextBackoffMillis);

      currentRetry += 1;

      return nextBackoffMillis;

    }



    private BackoffImpl(FluentBackoff backoffConfig) {

      this.backoffConfig = backoffConfig;

      this.reset();

    }



    @Override

    public String toString() {

      return MoreObjects.toStringHelper(BackoffImpl.class)

          .add("backoffConfig", backoffConfig)

          .add("currentRetry", currentRetry)

          .add("currentCumulativeBackoff", currentCumulativeBackoff)

          .toString();

    }

  }



  private FluentBackoff(

      double exponent,

      Duration initialBackoff,

      Duration maxBackoff,

      Duration maxCumulativeBackoff,

      int maxRetries) {

    this.exponent = exponent;

    this.initialBackoff = initialBackoff;

    this.maxBackoff = maxBackoff;

    this.maxRetries = maxRetries;

    this.maxCumulativeBackoff = maxCumulativeBackoff;

  }

}